package com.coolpad.job.device.controller;

import com.coolpad.MQ.kafka.ProducerHelpers;
import com.coolpad.Utils.GeoHashUtils;
import com.coolpad.Utils.Md5Utils;
import com.coolpad.job.device.model.RequestDTO;
import com.coolpad.job.device.model.Response;
import com.coolpad.job.device.model.ResponseDTO;
import com.coolpad.job.device.util.Constants;
import com.coolpad.job.device.util.JsonUtils;
import com.coolpad.job.device.util.SecurityUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@RestController
public class DailyUpdate {

    private static final ProducerHelpers producerHelpers = new ProducerHelpers();
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final String DEVICE_INFO_TOPIC = "device-info-daily-update";
    private static final String APP_USAGE_TOPIC = "app-usage-daily_update";
    private static final SimpleDateFormat DATE_FORMAT_PARTITION = new SimpleDateFormat("yyyy-MM-dd");

    private static final List<String> deviceInfoNamesInHive = Arrays.asList(
            Constants.DEVICE_INFO.IMEI1, Constants.DEVICE_INFO.IMEI1_MD5, Constants.DEVICE_INFO.IMEI2,
            Constants.DEVICE_INFO.IMEI2_MD5, Constants.DEVICE_INFO.MEID, Constants.DEVICE_INFO.MEID_MD5,
            Constants.DEVICE_INFO.IMSI, Constants.DEVICE_INFO.IMSI_MD5, Constants.DEVICE_INFO.RADIO,
            Constants.DEVICE_INFO.MODEL, Constants.DEVICE_INFO.BOARD, Constants.DEVICE_INFO.BRAND,
            Constants.DEVICE_INFO.REGION, Constants.DEVICE_INFO.LOCALE, Constants.DEVICE_INFO.MAC_ADDR,
            Constants.DEVICE_INFO.MAC_ADDR_MD5, Constants.DEVICE_INFO.BLUETOOTH_ADDR,
            Constants.DEVICE_INFO.BLUETOOTH_ADDR_MD5, Constants.DEVICE_INFO.SN,
            Constants.DEVICE_INFO.SN_MD5, Constants.DEVICE_INFO.ANDROID_ID, Constants.DEVICE_INFO.PHONE_NUMBER,
            Constants.DEVICE_INFO.GPS, Constants.DEVICE_INFO.GPS_GEO_HASH, Constants.DEVICE_INFO.IP_ADDR,
            Constants.DEVICE_INFO.ROM, Constants.DEVICE_INFO.RAM, Constants.DEVICE_INFO.AVAILABLE_ROM,
            Constants.DEVICE_INFO.USER_ACCOUNT, Constants.DEVICE_INFO.OS_VERSION, Constants.DEVICE_INFO.ANDROID_VERSION);

    private static final List<String> appUsageCommonNamesInHive = Arrays.asList(
            Constants.APP_USAGE.IMEI1_MD5, Constants.APP_USAGE.MODEL,
            Constants.APP_USAGE.ANDROID_ID, Constants.APP_USAGE.USER_ACCOUNT, Constants.APP_USAGE.IP,
            Constants.APP_USAGE.REGION, Constants.APP_USAGE.LOCALE, Constants.APP_USAGE.OS_VERSION,
            Constants.APP_USAGE.ANDROID_VERSION, Constants.APP_USAGE.NETWORK);
    private static final List<String> appUsageDetailNamesInHive = Arrays.asList( Constants.APP_USAGE.PACKAGE_NAME,
            Constants.APP_USAGE.APP_VERSION, Constants.APP_USAGE.INSTALL_SOURCE);

    private static final List<String> appUsageDetailNumNamesInHive = Arrays.asList( Constants.APP_USAGE.FREQUENCY_ALL,
            Constants.APP_USAGE.DURATION_ALL, Constants.APP_USAGE.DURATION_FRONT,
            Constants.APP_USAGE.DURATION_VALID, Constants.APP_USAGE.ACTIVATED);

    private static final String   COORDINATES_SEPARATOR = "#";
    @RequestMapping(value = "/update/service-check/echo/{string}", method = RequestMethod.GET)
    public String echo(@PathVariable String string) {
        return string;
    }

    @SuppressWarnings("unchecked")
    @RequestMapping(value = "/update/device-info/daily/echo", method = RequestMethod.POST)
    public ResponseDTO EchoUpdateDeviceInfoDaily(@RequestBody RequestDTO requestDTO) {

        String body = SecurityUtils.decryptRequest2Json(requestDTO);
        log.info("[EchoUpdateDeviceInfoDaily] Request parameter {}", body);
        return Response.success.build();
    }

    @SuppressWarnings("unchecked")
    @RequestMapping(value = "/update/app-usage/daily/echo", method = RequestMethod.POST)
    public ResponseDTO EchoUpdateAppUsageDaily(@RequestBody RequestDTO requestDTO) {

        String body = SecurityUtils.decryptRequest2Json(requestDTO);
        log.info("[EchoUpdateDeviceInfoDaily] Request parameter {}", body);
        return Response.success.build();
    }

    @RequestMapping(value = "/update/device-info/daily", method = RequestMethod.POST)
    public Map UpdateDeviceInfoDaily(@RequestBody RequestDTO requestDTO) {

        JsonNode body = JsonUtils.stringToNode(SecurityUtils.decryptRequest2Json(requestDTO));
        log.info("[UpdateDeviceInfoDaily] Request body {}", body.toString());

        String errTxt = null;

        do {
            try {
                // imei1 for sharding
                JsonNode shardingImei1 = body.get(Constants.DEVICE_INFO.IMEI1);
                JsonNode updateTime = body.get(Constants.DEVICE_INFO.UPDATE_TS);
                JsonNode deviceInfo = body.get("device_info");

                if (shardingImei1 == null || StringUtils.isEmpty(shardingImei1.asText()) ||
                        updateTime == null || updateTime.asLong() <= 0 || deviceInfo == null) {
                    errTxt = String.format("element [%s] , [%s] or [%s] not exist",
                            "Imei1", "updateTime", "deviceInfo");
                    log.warn(errTxt);
                    break;
                }

                Pair<Boolean, String> retHiveInsertTxt = flatDeviceInfoAsText(deviceInfo);
                if(!retHiveInsertTxt.getKey()) {
                    errTxt = retHiveInsertTxt.getValue();
                    log.warn(errTxt);
                    break;
                }

                log.info("APP USAGE In hive format {}", retHiveInsertTxt.getValue());

                producerHelpers.publish(DEVICE_INFO_TOPIC, retHiveInsertTxt.getValue(), shardingImei1.asText());

                return new HashMap<String, String>(){{
                    put("res", "succ");
                    put("echo", retHiveInsertTxt.getValue());
                }};

            } catch (Exception e) {
                System.out.println(String.format("Exception: %s", e));
            }
        }while(false);

        String finalErrTxt = errTxt;
        return new HashMap<String, String>(){{
            put("res", "fail");
            put("echo", finalErrTxt);
        }};
    }

    @RequestMapping(value = "/update/app-usage/daily", method = RequestMethod.POST)
    public Map UpdateAppUsageDaily(@RequestBody RequestDTO requestDTO) {

        JsonNode body = JsonUtils.stringToNode(SecurityUtils.decryptRequest2Json(requestDTO));
        log.info("[UpdateAppUsageDaily] Request body {}", body.toString());

        String errTxt = null;
        do {
            try {
                JsonNode imei1md5 = body.get(Constants.APP_USAGE.IMEI1_MD5);
                JsonNode updateTime = body.get(Constants.APP_USAGE.UPDATE_TS);
                JsonNode model = body.get(Constants.APP_USAGE.MODEL);
                JsonNode usages = body.get("usages");

                if (imei1md5 == null || StringUtils.isEmpty(imei1md5.asText()) ||
                        updateTime == null || updateTime.asLong() <= 0 ||
                        model == null || StringUtils.isEmpty(model.asText()) ||
                        usages == null || !usages.isArray()) {
                    errTxt = String.format("element [%s] , [%s] or [%s] not exist",
                            "imei_md5", "updateTime", "model", "packages");
                    log.warn(errTxt);
                    break;
                }

                Pair<Boolean, List<String>> retHiveInsertTxt = flatAppUsageAsList(body);
                if(!retHiveInsertTxt.getKey()) {
                    errTxt = "Convert incoming data to AppUsageDailyUpdate Exception: " + body.toString();
                    log.warn(errTxt);
                    break;
                }

                log.info("APP USAGE In hive format {}", retHiveInsertTxt.getValue());

                for(int i = 0; i < retHiveInsertTxt.getValue().size(); i++) {
                    producerHelpers.publish(APP_USAGE_TOPIC, retHiveInsertTxt.getValue().get(i), imei1md5.asText());
                }

                return new HashMap<String, String>(){{
                    put("res", "succ");
                    put("echo", body.toString());
                }};

            } catch (Exception e) {
                System.out.println(String.format("Exception: %s", e));
            }
        }while(false);

        String finalErrTxt = errTxt;
        return new HashMap<String, String>(){{
            put("res", "fail");
            put("echo", finalErrTxt);
        }};
    }

    public static Pair<Boolean, List<String>> flatAppUsageAsList(JsonNode appUsages) {
        List<String> retValue = new ArrayList<>();
        boolean res = false;

        do {
            try {
                // 把原始数据中的","用空格替换
                Map<String, String> head = appUsageCommonNamesInHive.stream()
                        .collect(Collectors.toMap(key -> key, key -> (appUsages.get(key) == null ?
                                String.format("") : appUsages.get(key).asText().replaceAll(",", " "))));

                JsonNode usages = appUsages.get("usages");
                JsonNode updateTs = appUsages.get(Constants.APP_USAGE.UPDATE_TS);

                Iterator<JsonNode> it = usages.elements();
                while(it.hasNext()) {
                    JsonNode usage = it.next();
                    JsonNode date = usage.get(Constants.APP_USAGE.DATE);
                    JsonNode packages = usage.get("packages");

                    if(date == null || StringUtils.isEmpty(date.asText()) ||
                            packages == null || !packages.isArray() ) {
                        log.warn("Invalid date or empty packages, skip.");
                        continue;
                    }

                    Iterator<JsonNode> itPkg = packages.elements();
                    while(itPkg.hasNext()) {
                        JsonNode pkg = itPkg.next();
                        JsonNode pkgName = pkg.get(Constants.APP_USAGE.PACKAGE_NAME);
                        JsonNode appVer = pkg.get(Constants.APP_USAGE.APP_VERSION);

                        if(pkgName == null || StringUtils.isEmpty((pkgName.asText())) ||
                                appVer == null || StringUtils.isEmpty((appVer.asText()))) {
                            log.warn("Found empty package name or app version, skip.");
                            continue;
                        }

                        Map<String, String> tail = appUsageDetailNamesInHive.stream()
                                .collect(Collectors.toMap(key -> key, key -> (pkg.get(key) == null ?
                                        String.format("") : pkg.get(key).asText().replaceAll(",", " "))));

                        // 文本值可以用这种方式处理
                        tail.put(Constants.APP_USAGE.DATE, date.asText());
                        tail.putAll(head);
                        String usageValue = MAPPER.writeValueAsString(tail);

                        // 处理非文本值
                        String NumValues = "," + appUsageDetailNumNamesInHive.stream().map(e -> pkg.get(e) == null ?
                                "\"" + e + "\":\"\"" : "\"" + e + "\":" + pkg.get(e).asLong()).collect(Collectors.joining(","));
                        NumValues += ",\"" + Constants.APP_USAGE.UPDATE_TS + "\":" + updateTs.asLong();

                        StringBuffer sb = new StringBuffer(usageValue);
                        sb.insert(usageValue.length() - 1, NumValues);

                        retValue.add(sb.toString());
                    }
                    res = true;

                }

            } catch (Exception e) {
                log.warn(String.format("Convert incoming data to AppUsageDailyUpdate Exception: {}", e));
            }
        } while(false);
        return Pair.of(res, retValue);
    }

    /**
     *
     * @param deviceInfo
     * @return true if extract all element and format as String, delimited by ","
     * false if incorrect format or essential elements are missing
     */
    public static Pair<Boolean, String> flatDeviceInfoAsText(JsonNode deviceInfo) {
        String retValue = null;
        boolean res = false;
        do {
            try {
                // MUST HAVE imei1和meid必须有一个有值
                // model 和 更新时间为必须参数
                JsonNode imei1 = deviceInfo.get(Constants.DEVICE_INFO.IMEI1);
                JsonNode meid = deviceInfo.get(Constants.DEVICE_INFO.MEID);
                JsonNode model = deviceInfo.get(Constants.DEVICE_INFO.MODEL);
                JsonNode updateTs = deviceInfo.get(Constants.DEVICE_INFO.UPDATE_TS);
                if((imei1 == null && meid == null) || model == null || updateTs == null) {
                    retValue = String.format("DeviceInfo [%s] , [%s] or [%s] not exist",
                            "imei/meid", "updateTime", "model");
                    log.warn(retValue);
                    res = false;
                    break;
                }
                if(!((imei1 != null && IsLegalImei(imei1.asText())) ||
                        (meid != null && IsLegalMeid(meid.asText())))) {
                    retValue = String.format("Illegal [imei] or [meid]");
                    res = false;
                    break;
                }

                String updateDate = DATE_FORMAT_PARTITION.format(updateTs.asLong());

                // ","为Hive 列分隔符， 转化时把原始数据中的","以空格替换
                Map<String, String> values = deviceInfoNamesInHive.stream()
                        .collect(Collectors.toMap(key -> key, key -> (deviceInfo.get(key) == null ?
                                String.format("") : deviceInfo.get(key).asText().replaceAll(",", " "))));

                values.put(Constants.DEVICE_INFO.IMEI1_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.IMEI1)));
                if(IsLegalImei(values.get(Constants.DEVICE_INFO.IMEI2))) {
                    values.put(Constants.DEVICE_INFO.IMEI2_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.IMEI2)));
                }
                if(IsLegalImei(values.get(Constants.DEVICE_INFO.MEID))) {
                    values.put(Constants.DEVICE_INFO.MEID_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.MEID)));
                }
                if(IsLegalImei(values.get(Constants.DEVICE_INFO.IMSI))) {
                    values.put(Constants.DEVICE_INFO.IMSI_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.IMSI)));
                }
                if(IsLegalSn(values.get(Constants.DEVICE_INFO.SN))) {
                    values.put(Constants.DEVICE_INFO.SN_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.SN)));
                }
                if(IsLegalImei(values.get(Constants.DEVICE_INFO.MAC_ADDR))) {
                    values.put(Constants.DEVICE_INFO.MAC_ADDR_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.MAC_ADDR)));
                }
                if(IsLegalImei(values.get(Constants.DEVICE_INFO.BLUETOOTH_ADDR))) {
                    values.put(Constants.DEVICE_INFO.BLUETOOTH_ADDR_MD5, Md5Utils.md5(values.get(Constants.DEVICE_INFO.BLUETOOTH_ADDR)));
                }

                // 计算geo hash值，长度10位，换算精度0.6米
                String gps = values.get(Constants.DEVICE_INFO.GPS);
                if(!StringUtils.isEmpty(gps) && gps.contains(COORDINATES_SEPARATOR)) {
                    String [] coordinaters = StringUtils.split(gps, COORDINATES_SEPARATOR);
                    if(coordinaters.length == 2) {
                        double longitude = Double.valueOf(coordinaters[0]);
                        double latitude = Double.valueOf(coordinaters[1]);

                        String geoHash = GeoHashUtils.geoHash(longitude, latitude, GeoHashUtils.Constants.DEFAULT_GEO_HASH_CODE_LEN);

                        values.put(Constants.DEVICE_INFO.GPS_GEO_HASH, geoHash);
                    }
                }

                values.put(Constants.DEVICE_INFO.UPDATE_DATE, updateDate);
                // JSON
                retValue = MAPPER.writeValueAsString(values);

                String ts = ",\"" + Constants.DEVICE_INFO.UPDATE_TS + "\":" + updateTs.asLong();
                StringBuffer sb = new StringBuffer(retValue);
                sb.insert(retValue.length() - 1, ts);

                retValue = sb.toString();
                res = true;
            } catch (Exception e) {
                log.warn(String.format("Convert incoming data to DeviceInfoDailyUpdate Exception: {}", e));
            }
        }while(false);

        return Pair.of(res, retValue);
    }

    public static boolean IsLegalSn(String sn) {
        if(StringUtils.isEmpty(sn)) {
            return false;
        }

        return true;
    }

    public static boolean IsLegalImei(String imei) {
        if(StringUtils.isEmpty(imei)) {
            return false;
        }
        if(imei.length() < 15 || imei.length() > 17) {
            return false;
        }
        return true;
    }

    public static boolean IsLegalMeid(String meid) {
        if(StringUtils.isEmpty(meid)) {
            return false;
        }
        if(meid.length() < 14 || meid.length() > 15) {
            return false;
        }
        return true;
    }

    public static boolean IsLegalMd5(String md5) {
        if(StringUtils.isEmpty(md5)) {
            return false;
        }
        return true;
    }

    public static boolean IsLegalPhoneNumber(String phoneNumber) {
        if(StringUtils.isEmpty(phoneNumber)) {
            return false;
        }
        return true;
    }
}
